<?php
namespace \DragonU\DB;
/**
 * DragonU DB library.
 *
 * @internal
 * Copyright (c) 2009, Jacob Santos
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted
 * provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright notice, this list of conditions
 *    and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice, this list of
 *    conditions and the following disclaimer in the documentation and/or other materials provided
 *    with the distribution.
 *  * Neither the name of the DragonU nor the names of its contributors may be used to endorse or
 *    promote products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author Jacob Santos <wordpress@santosj.name>
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD license
 * @package DragonU
 * @subpackage DB
 * @since 0.1
 */

/**
 * DragonU DB class.
 *
 * Uses PDO, so any loaded PDO extension library can be used. This means that
 * SQLite is supported as well as MySQL. It does not mean that the SQL is
 * supported for that database system.
 *
 * Automatically accepts WordPress Database Constants, if they are available.
 * However, the DSN should be given. It shouldn't be difficult to build that
 * into the installer.
 *
 * The class does not fully implement the Active Record nor CRUD class patterns
 * and is not meant to be used as such. There are already many, better written
 * libraries that handle those patterns. This class is lower level as close to
 * the Database as possible. Therefore, queries will not be rewritten and only
 * minimal implementation of Active Record type features will ever be done.
 *
 * Support for Schemes will come later, after the main implementation is
 * completed and an easy to use and efficient pattern is developed.
 *
 * @author Jacob Santos
 * @since 0.1
 * @package DragonU
 * @subpackage DB
 */
class DB
{

	/**
	 * Database collation.
	 *
	 * @since 0.1
	 * @var string
	 */
	protected $collation = '';

	/**
	 * Database charset.
	 *
	 * @since 0.1
	 * @var string
	 */
	protected $charset = '';

	/**
	 * Stores the DB Caching object.
	 *
	 * @since 0.1
	 * @var \DragonU\DB\Cache
	 */
	protected static $cache = null;

	/**
	 * Stores the DB Connector object.
	 *
	 * @since 0.1
	 * @var \DragonU\DB\Connector
	 */
	protected static $connector = null;

	/**
	 * Stores the DB Debug object.
	 *
	 * @since 0.1
	 * @var \DragonU\DB\Debug
	 */
	protected static $debug = null;

	/**
	 * Stores the DB Statistics Object.
	 *
	 * @since 0.1
	 * @var \DragonU\DB\Statistics
	 */
	protected static $statistics = null;

	/**
	 * Stores the DB Table Object.
	 *
	 * @since 0.1
	 * @var \DragonU\DB\Table
	 */
	protected static $table = null;

	/**
	 * Setup static variables and connect to Database.
	 *
	 * @since 0.1
	 *
	 * @return \DragonU\DB
	 */
	public function __construct()
	{
		static::$table = new Table();
		static::$connector = new Connector();
		static::$cache = new Cache();
		static::$statistics = new Statistics();
		static::$debug = new Debug();
	}

	/**
	 * Retrieve caching object.
	 *
	 * @since 0.1
	 *
	 * @return \DragonU\Cache()
	 */
	public function cache()
	{
		return static::$cache;
	}

	/**
	 * Retrieve connector object.
	 *
	 * @since 0.1
	 *
	 * @return \CorePress\Connector()
	 */
	public function connect()
	{
		return static::$connector;
	}

	/**
	 * Retrieve debug object.
	 *
	 * @since 0.1
	 *
	 * @return \DragonU\DB\Debug()
	 */
	public function debug()
	{
		return static::$debug;
	}

	/**
	 * Retrieve statistics object.
	 *
	 * @since 0.1
	 *
	 * @return \DragonU\DB\Statistics()
	 */
	public function stats()
	{
		return static::$statistics;
	}

	/**
	 * Retrieve table object.
	 *
	 * @since 0.1
	 *
	 * @return \DragonU\DB\Table()
	 */
	public function table()
	{
		return static::$table;
	}

	/**
	 * Execute SQL query and retrieve rows, if any.
	 *
	 * @since 0.1
	 *
	 * @param string $sql
	 * @return null|\DragonU\Process Return nothing, unless select query.
	 */
	public function query($sql)
	{
		if( strtolower( substr($sql, 0, 6) ) == 'select' )
		{
			$db = $this->connect()->db();
			$statement = $db->query($sql);
			$results = $statement->fetchAll(PDO::FETCH_OBJ);


		}
		else
		{
			$affected = array();

			foreach( $this->connect() as $db )
			{
				$affected[] = $db->exec($sql);
			}

			return $affected;
		}
	}

	/**
	 * Prepare SQL query.
	 *
	 * This is direct abstraction of PDO Prepared Statements. Actually, the
	 * PDO Prepared Statement will be returned. Eventually, a class may be
	 * developed which abstracts the PDO Prepared Statements further, but not
	 * at this stage.
	 *
	 * @since 0.1
	 *
	 * @param string $sql
	 * @return PDOStatement
	 */
	public function prepare($sql)
	{

	}

	/**
	 * Execute insert query.
	 *
	 * If the $value parameter array is passed a value, then the query will be
	 * prepared and bound to that type. If no value is passed, then the values
	 * will be escaped and inserted into the query directly.
	 *
	 * Basically, you should think fo the $field parameter array as the array
	 * keys for the $value parameter array. If you want to pass the type, then
	 * create an array inside the array with the indices 'value' and 'type' with
	 * the value and type respectively.
	 *
	 * @todo This could be abstracted out further to make it easier, but don't
	 *     want to fall to far into the Active Record pattern.
	 *
	 * @todo Need to use a database scheme for the update.
	 *
	 * @since 0.1
	 *
	 * @param string $table Table name
	 * @param array $fields List of fields.
	 * @param array $values List of values that match the fields.
	 * @return int Inserted ID.
	 */
	public function insert($table, array $fields, array $values)
	{
		$sql = 'INSERT INTO `'. $table .'` (';

		foreach( $fields as $field )
		{
			$sql .= '`'. $field .'`,';
		}

		$sql .= ') VALUES(';

		$max = count($fields);

		for( $i=1; $i <= $max; ++$i )
		{
			$sql .= '?';

			if( $i !== $max )
			{
				$sql .= ', ';
			}
		}

		$sql .= ')';

		$insertID = 0;

		foreach( $this->connect() as $db )
		{

			try
			{
				$statement = $db->prepare($sql);
			}
			catch( \PDOException $e)
			{
				/** @todo Log exception */
				continue;
			}

			$this->bindValues($statement, $values, $max);

			$statement->execute();

			if( $insertID === 0 )
			{
				$insertID = $statement->lastInsertId();
			}
		}

		return $insertID;
	}

	/**
	 * Execute update query.
	 *
	 * The idea is that the update is prepared, so in the $value parameter array
	 * you can also specify the type. If you don't specify the type, then the
	 * type will be checked against the value type and prepared for you. It will
	 * be more secure, if the type is given.
	 *
	 * The list of types is as follows:
	 *
	 * <ol>
	 * <li>string</li>
	 * <li>int</li>
	 * <li>string</li>
	 * <li>text</li>
	 * <li>null</li>
	 * </ol>
	 *
	 * @todo Need to use a database scheme for the update.
	 *
	 * @param string $table Table name
	 * @param array $fields List of fields.
	 * @param array $values List of values that match the fields.
	 * @param string $where Optional. Clause for the update. Expected to be sanitized.
	 * @return int Affected rows.
	 */
	public function update($table, array $fields, array $values, $where = null)
	{
		$sql = 'UPDATE `'. $table .'` SET ';

		foreach( $fields as $field )
		{
			$sql .= '`'. $field .'` = ?, ';
		}

		$sql = trim($sql, ', ');

		if( isset( $where ) )
		{
			$sql .= ' WHERE '. $where;
		}

		$affected = 0;
		foreach( $this->connect() as $db )
		{

			try
			{
				$statement = $db->prepare($sql);
			}
			catch( \PDOException $e)
			{
				/** @todo Log exception */
				continue;
			}

			$this->bindValues($statement, $values, count($fields));

			$statement->execute();

			$affected = $statement->rowCount();
		}

		return $affected;
	}

	/**
	 * Execute delete query.
	 *
	 * @todo Need to use a database Scheme for the delete.
	 *
	 * @param string $table Table name
	 * @param string $where Optional. Clause for the update. Excepts to be sanitized.
	 * @param int $limit Optional, default is no limit. What to limit the delete at.
	 * @return int Affected rows.
	 */
	public function delete($table, $where = null, $limit = null)
	{
		if( ! isset($where) )
		{
			foreach( $this->connect() as $db )
			{
				$db->exec('TRUNCATE `'. $table .'`');
			}
			return null;
		}

		$sql = 'DELETE FROM `'. $table .'` WHERE '. $where;

		if( isset($limit) )
		{
			$sql .= ' LIMIT '. (int) $limit;
		}

		return $this->query($sql);
	}

	/**
	 * Escape value using PDO escape method.
	 *
	 * If there is no database connection, then the original value will be
	 * returned.
	 *
	 * @since 0.1
	 *
	 * @param mixed $value Field value.
	 * @return mixed
	 */
	public function escape($value)
	{
		$pdo = $this->connect()->db();

		if( $pdo instanceof \PDO )
		{
			return $pdo->escape($value);
		}

		return $value;
	}

	/**
	 * Retrieve and, or set database collation.
	 *
	 * @todo Need to work this into the database scheme implementation.
	 *
	 * @param string $collation
	 * @return string
	 */
	public function collation($collation = null)
	{
		if( is_string( $collation ) )
		{
			$this->collation = $collation;

			foreach( $this->connect() as $db )
			{
				$db->exec("SET CHARACTER NAMES ". $db->escape($collation));
			}
		}

		return $this->collation;
	}

	/**
	 * Retrieve and, or set database charset.
	 *
	 * @todo Need to work this into the database scheme implementation.
	 *
	 * @param string $charset
	 * @return string
	 */
	public function charset($charset = null)
	{
		if( is_string( $charset ) )
		{
			$this->charset = $charset;

			foreach( $this->connect() as $db )
			{
				$db->exec("charset ". $db->escape($charset));
			}
		}

		return $this->charset;
	}

	/**
	 * Retrieve database version.
	 *
	 * @return string
	 */
	public function version()
	{
		$db = $this->connect()->db();

		if( $db instanceof \PDO )
		{
			return $db->getAttribute(PDO::ATTR_SERVER_VERSION);
		}

		return '';
	}

	protected function bindValues($statement, $values, $max)
	{
		$i = 0;

		foreach( $values as $type => $value )
		{
			if( $i == $max)
			{
				break;
			}

			$done = false;
			if( is_string($type) )
			{
				switch( $type )
				{
					case 'bool':
						$statement->bindParam($i, $value, PDO::PARAM_BOOL);
						$done = true;
						break;
					case 'int':
						$statement->bindParam($i, $value, PDO::PARAM_INT);
						$done = true;
						break;
					case 'string':
						$statement->bindParam($i, $value, PDO::PARAM_STR, strlen($value)+1);
						$done = true;
						break;
					case 'text':
						$statement->bindParam($i, $value, PDO::PARAM_LOB);
						$done = true;
						break;
					case 'null':
						$statement->bindParam($i, $value, PDO::PARAM_NULL);
						$done = true;
						break;
					default:
				}
			}

			if( $done === false )
			{
				if( is_bool( $value ) )
				{
					$statement->bindParam($i, $value, PDO::PARAM_BOOL);
				}
				else if( is_numeric( $value ) )
				{
					$statement->bindParam($i, $value, PDO::PARAM_INT);
				}
				else if( is_string( $value ) )
				{
					$statement->bindParam($i, $value, PDO::PARAM_STR, strlen($value)+1);
				}
				else if( is_null( $value ) )
				{
					$statement->bindParam($i, $value, PDO::PARAM_NULL);
				}
				else if( is_object( $value ) || is_array( $value ) )
				{
					$value = serialize($value);
					$statement->bindParam($i, $value, PDO::PARAM_LOB);
				}
			}

			++$i;
		}
	}

}

